package com.imooc.activitiweb.controller;

import com.imooc.activitiweb.SecurityUtil;
import com.imooc.activitiweb.mapper.ActivitiMapper;
import com.imooc.activitiweb.util.AjaxResponse;
import com.imooc.activitiweb.util.GlobalConfig;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.bpmn.model.FormProperty;
import org.activiti.bpmn.model.UserTask;
import org.activiti.engine.RepositoryService;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 任务 控制类
 */
@RestController
@RequestMapping("/task")
public class TaskController {

    @Autowired
    private SecurityUtil securityUtil;
    @Autowired
    private TaskRuntime taskRuntime;
    @Autowired
    private ProcessRuntime processRuntime;
    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private ActivitiMapper activitiMapper;
    /**
     * 获取我的待办任务
     * @return
     */
    @GetMapping("/getTasks")
    public AjaxResponse getTasks(){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记，标记是测试环境使用 内存用户登录，方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 获取任务列表
            Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 100));
            List<Map<String,Object>> listMap = new ArrayList<Map<String,Object>>();
            for (Task task : tasks.getContent()){
                Map<String,Object> map = new HashMap<String, Object>();
                map.put("id", task.getId());
                map.put("name",task.getName());
                map.put("status",task.getStatus());
                map.put("createDate",task.getCreatedDate());
                /**
                 * 执行人这里有一个重要的细节
                 *      这个方法查询的是当前登录人所有的任务，task.getAssignee() 查询的就是实际登录人的名称
                 *      实际上这个方法不光是执行是当前登录人的所有任务，他还包括候选人是当前登录人的所有任务，
                 *      只不过如果候选人是当前登录用户 task.getAssignee() 返回的是 null,这里如果 task.getAssignee()
                 *      返回的确实是当前登录人，那么 task.getAssignee() 就是当前登录人，否则我们和给返回一个带拾取任务
                 *      这样前端用户就很清楚的知道是分配给他的任务，还是说是一条带拾取的任务
                 */
                // 我们判断一下
                if(task.getAssignee() == null){
                    map.put("assignee","带拾取任务"); // 这里先写死了
                }else{
                    map.put("assignee",task.getAssignee());  // 当前登录人
                }
                // 由于这里获取不到流程实例名称，我们需要查询一下
                ProcessInstance processInstance = processRuntime.processInstance(task.getProcessInstanceId());
                // 保存流程实例名称
                map.put("instanceName",processInstance.getName());
                listMap.add(map);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "获取我的任务列表失败",
                    e.toString()
            );
        }
    }

    /**
     * 完成任务
     * @return
     */
    @GetMapping("/completeTask")
    public AjaxResponse completeTask(@RequestParam("taskId")String taskId){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记，标记是测试环境使用 内存用户登录，方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);
            // task.getAssignee() == null 说明这是一条带拾取的任务，我们先拾取在完成
            if(task.getAssignee() == null){
                taskRuntime.claim(TaskPayloadBuilder
                        .claim()
                        .withTaskId(taskId)
                        .build()
                );
            }
            // 完成任务  注意：挂起的任务需要先激活在完成
            taskRuntime.complete(
                    TaskPayloadBuilder
                            .complete()
                            .withTaskId(taskId)
                            //.withVariable("xxx","xx") 完成任务也是可以设置实例参数的
                            .build()
            );
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    null
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "完成任务 "+taskId+" 失败",
                    e.toString()
            );
        }
    }

    /**
     * 渲染动态表单
     * @return
     */
    @GetMapping("/formDataShow")
    public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记，标记是测试环境使用 内存用户登录，方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);

            /*
                构建表单控件历史数据字典
                key 是控件的 id
                value 是控件的值
                这里是我们根据 taskId 查询出来的所有流程的表单数据
            */
            Map<String,String> controlListMap = new HashMap<String, String>();
            // 读取数据库本流程实例的所有表单数据
            List<Map<String, String>> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
            // 将查询出来的控件 id 和值保存在数据字典中
            for(Map<String,String> map : tempControlList){
                controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
            }

            /**
             * 关键代码，这里在强调一下，在 activiti6 和 5 的时候实际上是有 form 这个类的
             * 但是在 activiti7 中去掉的为了轻量化，但是我们在 7 中还是有方法可以通过流程
             * 定义的 id 和任务的 id 拿到一个叫 userTask 的类，在 userTask 类中可以拿到表单属性
             */
            UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
                    /**
                     * 获取流程元素，这里是要传什么呢？这里实际上是要传任务的key的，在 task 里并没有任务的 key 的
                     * 在 activiti 6 中是有任务的 key 的，不过我们可以用另外一个方案，我们可以用表单的 key ，这里
                     * 我们可以表表单的 key 和任务的 key 启成一模一样的名字，这样你拿表单的 key 就相当于拿任务的 key
                     * 了
                     */
                    .getFlowElement(task.getFormKey());
            // 说明该环节是不需要表单的
            if(userTask == null){
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.SUCCESS.getCode(),
                        GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                        "无表单"
                );
            }

            List<FormProperty> formProperties = userTask.getFormProperties();
            // 保存分割后的格式返回给前端
            List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
            for (FormProperty formProperty : formProperties) {
                // 分割表单数据
                String[] split = formProperty.getId().split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                form.put("id", split[0]);
                form.put("controlType", split[1]);
                form.put("controlLabel", split[2]);

                // 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
                if(split[3].startsWith("FormProperty_")){
                    // 是表单数据  从之前的保存的所有控件历史数据字典中读取
                    // 在这里  split[3] 拿到的就是历史表单的 key
                    if(controlListMap.containsKey(split[3])){
                        form.put("controlDefValue", controlListMap.get(split[3]));
                    }else{
                        // 如果字典中不存在给出错误提示
                        form.put("controlDefValue", "读取失败，检查"+split[3]+"配置");
                    }
                }else{
                    // 不是之前表单数据
                    form.put("controlDefValue", split[3]);
                }

                form.put("controlParam", split[4]);
                listMap.add(form);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }

    /**
     * 保存动态表单
     * @param taskId      任务 id
     * @param formData    控件组字符串
     * @return
     */
    @PostMapping("/formDataSave")
    public AjaxResponse formDataSave(@RequestParam("taskId")String taskId,
                                     @RequestParam("formData")String formData){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记，标记是测试环境使用 内存用户登录，方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);

            // 保存参数
            Map<String,Object> variables = new HashMap<String, Object>();
            // 用来表示是否有参数，已执行响应的代码
            boolean hasVariables = false;

            // 前端传来的字符串拆分为控件组
            String[] formDataList = formData.split("!_!");
            // 保存解析后的数据
            List<Map<String,Object>> controlItems = new ArrayList<Map<String, Object>>();
            for (String control : formDataList) {
                // 分割表单数据
                String[] split = control.split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                // 流程定义 key
                form.put("PROC_DEF_ID_", task.getProcessDefinitionId());
                // 流程实例 key
                form.put("PROC_INST_ID_", task.getProcessInstanceId());
                // 表单 key
                form.put("FORM_KEY_", task.getFormKey());
                // 控件 key
                form.put("Control_ID_", split[0]);
                // 控件值
                form.put("Control_VALUE_", split[1]);
                // 是否是流程 UEL 参数 不需要存到数据库
                form.put("Control_PARAM", split[2]);
                controlItems.add(form);

                // 构建参数集合
                switch (split[2]){
                    case "f":
                        System.out.println("控件值不作为参数");
                        break;
                    case "s":
                        // string 类型
                        // key 是控件的 id ,值是控件的值
                        // 为什么这么写呢？因为我们在配置 UEL 表达式的时候，比如说之前输入的 day > 3 ，这个 day 就是变量
                        // 那么这个 day 如何和我们的控件对应起来呢，这里我们把 day 这个变量直接换成控件的 id，所以这就又
                        // 是一个约束，在设置 UEL 表达式的时候变量名就需要是我们控件的 id 值就是用户输入的值，这样就可以
                        // 实现 UEL 表大式的动态配置
                        variables.put(split[0],split[1]);
                        hasVariables = true;
                        break;
                    case "t":
                        // 时间类型
                        SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm");
                        variables.put(split[0],dateFormat.parse(split[1]));
                        hasVariables = true;
                        break;
                    case "b":
                        // boolean 类型
                        variables.put(split[0], BooleanUtils.toBoolean(split[1]));
                        hasVariables = true;
                        break;
                    default:
                        System.out.println("控件ID:"+split[0]+"的参数"+split[1]+"不存在");
                }
            }

            // 完成任务
            /**
             * 在这里强调一下使用 taskService 完成任务经过反复测试，后面设置的变量不会覆盖之前设置的变量，
             * 这不是我们想要的，我们需要后面的设置的变量要覆盖掉前面的变量值，经过测试 activiti7 中
             * taskRuntime 是可以的
             */
            if(hasVariables){
                // 有参数
                taskRuntime.complete(TaskPayloadBuilder
                        .complete()
                        .withTaskId(taskId)
                        .withVariables(variables)  // 设置变量
                        .build()
                );
            }else{
                // 没有参数
                taskRuntime.complete(TaskPayloadBuilder
                        .complete()
                        .withTaskId(taskId)
                        .build()
                );
            }

            // 将表单写入数据库
            activitiMapper.insertFormData(controlItems);
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    controlItems
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }
}
